package org.vacoor.nothing.web.service.support.storage;

import org.apache.commons.io.FilenameUtils;
import org.im4java.core.*;
import org.im4java.process.OutputConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.vacoor.nothing.common.codec.Base64;
import org.vacoor.nothing.common.io.NullOutputStream;
import org.vacoor.nothing.common.util.Bytes;
import org.vacoor.nothing.common.util.IOUtils;
import org.vacoor.nothing.web.service.ImageStorageService;
import org.vacoor.nothing.common.util.Throwables;
import org.vacoor.nothing.web.util.Images;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.MessageFormat;
import java.util.List;

/**
 * 图片存储服务
 * <p/>
 * 图片存储主要头疼的问题: 需要生成不同规格的缩略图
 * 不同的应用位置 和 不同的场景 一张图可能需要生成很多个不同规格的缩略图
 * 如果考虑可扩展性规格大小更是不能确定 (eg: 改版,开发其他终端)
 * <p/>
 * 这类图片一般用户不关心名称
 * 这里将文件的规格信息(暂时只有尺寸)存在文件名中, 使得文件名包含更多元信息
 * 缩略图与原始文件在同一位置存放, 在引用时只需要引用原始文件标识(这里为了省事直接返回 path),
 * eg: original.jpg 获取该图片30x30规格 访问 original.jpg_30x30.jpg 即可
 * 这样做的好处:
 * 1. 获取不同规格时根据原始文件+规格表达式 获取即可, 免去对所有规格引用并且更加灵活
 * 2. 避免了繁琐的文件系统目录的处理 (可能需要按规格分目录, 以及目录容量均衡处理)
 * 3. 日后改版等只需要修改显示位置参数即可
 * <p/>
 * 缩略图根据规则延迟生成(第一次请求), 这样做好处:
 * 1. 根据实际需要生成, 避免占用太多空间
 * 2. 缩略图可以根据实际需要生成, 更加灵活(eg: 30x30, 40x40)
 * <p/>
 * 返回标示为 Base64.encodeUrlSafeString(path) + filename 方便URL使用及设置缓存
 * <p/>
 * TODO 淘宝 TFS 好像也是类似思路, 有空看下
 * TODO 处理不依赖 gm
 * <p/>
 * 注: <br />
 * JDK 原生 ImageIO.read/write 不支持 cmyk 模式的 jpg会丢失 ICC 信息的图片(读取后有红色蒙版)
 * http://stackoverflow.com/questions/6829428/pure-java-alternative-to-jai-imageio-for-detecting-cmyk-images
 * 解决方案: <br />
 * 1. 使用 https://github.com/haraldk/TwelveMonkeys (实现 java image spi, 操作不用任何变化)
 * 2. GraphicsMagic + im4java (需要安装 GM, 效率高, 且资源占用低, 推荐使用)
 *
 * @author vacoor
 */
@Service
public class ImageLocalStorageServiceImpl extends DailyRollingStorageService implements ImageStorageService {
    private static final Logger LOG = LoggerFactory.getLogger(ImageLocalStorageServiceImpl.class);

    public static final String TEMP_FILE_PREFIX = ImageLocalStorageServiceImpl.class.getName() + ".temp-file-";
    public static final String TEMP_FILE_SUFFIX = ".tmp";
    public static final String ENCODE_PREFIX = "ILS";               // encode 前缀
    public static final String ENCODE_SEPARATOR = "!!";             // encode 分隔符
    public static final String SIZE_SEPARATOR = "x";                // 文件尺寸分隔符
    public static final String THUMB_NAME_FMT = "{0}_{1}x{2}.{3}";  // 缩略图命名规则

    public static final String DEFAULT_FORMAT = "jpg";              // 默认图片格式

    private String format = DEFAULT_FORMAT;
    private List<String> allowSizeList;                             // 允许的缩略图尺寸

    public ImageLocalStorageServiceImpl() {
        setStorageHome(getStorageHome() + "/images");
    }

    /**
     * 获取给定路径的图片作为文件
     *
     * @param id 调用 store 方法时返回的路径或追加大小参数后的路径
     * @return
     */
    @Override
    public File readAsFile(String id) {
        id = getStorageRealPath(decodePath(id));    // 存储的绝对路径

        File file = new File(id);
        if (file.exists()) {
            if (!file.isFile()) {
                return null;
            }

            return file;
        }

        // 可能是需要生成缩略图的请求
        id = FilenameUtils.getFullPath(id);         // 请求的文件路径
        String filename = FilenameUtils.getName(id);  // 请求的文件名称

        int origNameLength = getOriginalNameLength(filename);      // 原始图片名称
        if (origNameLength == filename.length()) {                 // 不包含尺寸信息
            return null;
        }

        String origName = filename.substring(0, origNameLength);   // 未缩放的文件名
        String suffix = "." + FilenameUtils.getExtension(origName); // 文件后缀

        // 提取缩略图名字
        if (filename.endsWith(suffix)) {
            filename = filename.substring(origNameLength + 1, filename.length() - suffix.length());
        } else {
            filename = filename.substring(origNameLength + 1);
        }

        if (null != allowSizeList && allowSizeList.size() > 0 && !allowSizeList.contains(filename)) {
            LOG.debug("ignore invalid thumbnail: {} {}", origName, filename);
            return null;
        }

        // 尝试获取所缩略图大小
        String[] size = filename.split(SIZE_SEPARATOR);
        if (2 != size.length) {
            return null;
        }

        try {
            int width = Integer.parseInt(size[0]);
            int height = Integer.parseInt(size[1]);
            File thumb = new File(id, getThumbnailName(origName, width, height));
            if (thumb.exists()) {   // 缩略图已经存在
                return thumb;
            }

            File origFile = new File(id, origName);
            if (!origFile.exists()) {   // 原始文件不存在
                LOG.debug("ignore thumbnail, original file is not exists: {}", origFile);
                return null;
            }

            LOG.debug("generate thumbnail {}", thumb);

            // 生成缩略图
            generateThumb(origFile, thumb, width, height);

            return thumb;
        } catch (Exception e) {
            LOG.warn("generate thumbs failure {}", e);

            return null;
        }
    }

    @Override
    public String store(InputStream is) throws IOException {
        File path = new File(getStorageRealPath(getDailyRollingFolder()));
        if (!path.exists()) {
            path.mkdirs();
        }

        String name = generateFilename();
        File tmp = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
        File target = new File(path, name + "." + format);

        try {
            IOUtils.flow(is, new FileOutputStream(tmp), true, true);
            saveAs(tmp, target);

            return encodePath(getStorageRelativePath(target.getAbsolutePath()));
        } catch (Exception e) {
            LOG.warn("store image failure: {}", e);

            target.delete();
            throw new IOException(e);
        } finally {
            tmp.delete();
        }
    }

    /**
     * encode given file path
     *
     * @param path
     * @return
     */
    private String encodePath(String path) {
        path = !path.endsWith("/") ? path : path.substring(0, path.length() - 1);

        int i = path.lastIndexOf("/");
        if (-1 < i) {
            path = Base64.encodeToUrlSafeString(Bytes.toBytes(path.substring(0, i))) + ENCODE_SEPARATOR + path.substring(i + 1);
        }

        return ENCODE_PREFIX + path;
    }

    /**
     * decode {@link #encodePath(String)}
     *
     * @param encodedPath
     * @return
     */
    private String decodePath(String encodedPath) {
        int i = encodedPath.indexOf(ENCODE_SEPARATOR);
        if (-1 < i && encodedPath.startsWith(ENCODE_PREFIX)) {
            String fname = encodedPath.substring(i + 2);
            encodedPath = encodedPath.substring(ENCODE_PREFIX.length(), i);
            encodedPath = Bytes.toString(Base64.decode(encodedPath, true)) + "/" + fname;
        }
        return encodedPath;
    }

    /**
     * extract original file name length from given thumbnail name
     */
    private int getOriginalNameLength(String thumbName) {
        int len = thumbName.length();
        char[] chars = thumbName.toCharArray();

        for (int i = 0; i < chars.length - 1; i++) {
            char cur = chars[i];
            char next = chars[i + 1];

            if ('_' == cur && Character.isDigit(next)) {
                len = i;
                break;
            }
        }
        return len;
    }

    /**
     * build thumbnail name for original file name, width and height
     */
    private String getThumbnailName(String origName, int w, int h) {
        String suffix = FilenameUtils.getExtension(origName);
        return MessageFormat.format(THUMB_NAME_FMT, origName, w, h, suffix);
    }

    /**
     * 另存为
     *
     * @param source
     * @param target
     */
    protected void saveAs(File source, File target) {
        try {
            try {
                if (gmIsValid) {
                    IMOperation op = new IMOperation();
                    op.quality(90d);
                    op.addImage().addImage();

                    runIMConvertOps(op, source.getAbsolutePath(), target.getAbsolutePath());
                }
            } catch (Exception e) {
            }
            String absPath = target.getAbsolutePath();
            String format = absPath.substring(absPath.lastIndexOf(".") + 1);
            Images.write(Images.read(source), format, target);
        } catch (Exception e) {
            throw Throwables.unchecked(e);
        }
    }

    /**
     * 生成缩略图
     *
     * @param source
     * @param thumb
     * @param width
     * @param height
     */
    protected void generateThumb(File source, File thumb, int width, int height) {
        try {
            try {
                if (gmIsValid) {
                    IMOperation op = new IMOperation();
                    op.resize(width, height, "^").gravity("center").extent(width, height).quality(90d);
                    op.addImage().addImage();

                    runIMConvertOps(op, source.getAbsolutePath(), thumb.getAbsolutePath());
                }
            } catch (Exception e) {
            }

            BufferedImage orignal = Images.read(source);
            Image result = Images.scale(orignal, width, height, Image.SCALE_FAST | Image.SCALE_SMOOTH);
            Images.write(result, "JPG", thumb);
        } catch (IOException e) {
            throw Throwables.unchecked(e);
        }
    }

    /**
     * run ImageMagic/GraphicsMagic convert command
     */
    private void runIMConvertOps(IMOps ops, Object... args) throws InterruptedException, IOException, IM4JavaException {
        ConvertCmd cmd = new ConvertCmd(true);
        cmd.run(ops, args);
    }

    /* *************************
     *     getter / setter
     * *************************/

    public void setGraphicsMagicHome(String gmHome) {
        GraphicsMagickCmd.setGlobalSearchPath(gmHome);
    }

    public String getFormat() {
        return format;
    }

    public void setFormat(String format) {
        this.format = format;
    }

    public List<String> getAllowSizeList() {
        return allowSizeList;
    }

    public void setAllowSizeList(List<String> allowSizeList) {
        this.allowSizeList = allowSizeList;
    }


    /* ***************************************
     *
     * **************************************/

    private static boolean gmIsValid = false;
    private static boolean cmykSupported = false;

    static {
        try {
            // 测试 graphics magic
            ImageCommand gm = new ImageCommand("gm");
            gm.setOutputConsumer(NullOutputConsumer.INSTANCE);
            gm.run(new Operation().addRawArgs("-help"));

            gmIsValid = true;
            LOG.info("find GM (Graphics Magick)...");
        } catch (Exception ignore) { /* ignore */ }

        try {
            // 尝试 twelvemonkeys imageio (cmyk jpeg support)
            Thread.currentThread().getContextClassLoader().loadClass("com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi");

            cmykSupported = true;
            LOG.info("find \"twelyemonkeys imageio-jpeg\" plugins ...");
        } catch (ClassNotFoundException ignore) { /* ignore */ }

        if (!gmIsValid && !cmykSupported) {
            LOG.warn("Application is not support CMYK images (JPEG),");
            LOG.warn("because can't find \"twelyemonkeys imageio-jpeg\" plugins or \"GM (Graphics Magick)\",");
            LOG.warn("you can find them here, imageio-jpeg  https://github.com/haraldk/TwelveMonkeys, GM: http://www.graphicsmagick.org");
        }
    }

    private static class NullOutputConsumer implements OutputConsumer {
        public static final NullOutputConsumer INSTANCE = new NullOutputConsumer();

        @Override
        public void consumeOutput(InputStream inputStream) throws IOException {
            IOUtils.flow(inputStream, NullOutputStream.INSTANCE, true, true);
        }
    }

}
